Enforce :sharingan/:sharingan-noop public API parity in CI (#12)#29
Merged
Conversation
The release-swap safety story (consumer compiles against :sharingan in debug,
swaps :sharingan-noop in release) depends on the two independently-compiled
modules exposing a signature-for-signature identical public contract. Until now
nothing enforced that — a drifted default or a forgotten method would surface
only as a consumer's release-build compile error.
Add a `checkApiParity` Gradle task (root build) that compares the four committed
BCV dumps (JVM .api + native .klib.api, both modules) after filtering out the
symbols that legitimately exist in only the real module, and fails on any
bidirectional divergence:
- debug-only UI/platform classes the consumer never references directly
(dev/sharingan/ui/**, internal/**, SharinganActivity, ComposableSingletons$*,
and the native ui SharinganScreen composable);
- the Compose-compiler $stable field / $stableprop synthetics that only the
Compose-carrying real module emits — the gotcha that would false-fail a
naive diff since they live inside otherwise-shared classes;
- the klib `// Library unique name` header line, which differs by construction.
After filtering, both modules' 19 JVM contract classes (+ native
SharinganViewController/presentSharingan) must match exactly. The check is pure
text processing over committed files — no compilation, no iOS link — so it runs
in seconds with no Android/iOS SDK.
Wiring:
- registered in the `verification` group; runnable as `./gradlew checkApiParity`
- hooked into the root `check` lifecycle
- new `api-parity` CI job in api-check.yml on ubuntu (no macOS host needed)
Verified: red→green demonstrated on both surfaces (dropping a contract symbol
from a noop dump fails the check with a precise per-symbol diff; reverting
passes). `./gradlew apiCheck` confirms the committed dumps are current.
Docs: docs/api-parity.md explains the contract, the why, and every exclusion so
contributors keep the modules in lockstep; README contributing section points to
it.
Implementation note: the comparison helpers are local functions inside the task
action because Gradle Kotlin DSL silently drops top-level statements placed after
top-level `fun` declarations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12.
What & why
The release-swap safety story — consumer compiles against
:sharinganin debug, swaps:sharingan-noopin release — depends on the two independently-compiled modules exposing a signature-for-signature identical public contract. Until now nothing enforced that; a drifted default or a forgotten method would surface only as a consumer's release-build compile error, the worst place to find it.This adds a
checkApiParityGradle task that compares the four committed BCV dumps (JVM.api+ native.klib.api, both modules), applies an exclusion filter, and fails the build on any bidirectional divergence. It is pure text processing over already-committed files — no compilation, no iOS link, no Android/iOS SDK — so it runs in seconds.Approach (not a whole-file diff)
:sharinganlegitimately carries symbols the noop must not expose. These are filtered out before comparing (full rationale indocs/api-parity.md):dev/sharingan/ui/**,dev/sharingan/internal/**,SharinganActivity, anyComposableSingletons$*, and the native top-leveldev.sharingan.ui/SharinganScreencomposable.$stable/$stablepropsynthetics — the compiler stability metadata that ships only in the Compose-carrying real module. This is the subtle one: it lives inside otherwise-shared classes (e.g.HttpEvent), so a naive diff false-fails on every event type.// Library unique name:header, which differs by module name by construction.After filtering, both modules' 19 JVM contract classes (+ native
SharinganViewController/presentSharingan) must match exactly. Comparison is bidirectional: it fails if the noop is missing a real symbol and if it over-exposes one.Acceptance criteria
checkApiParityin the rootbuild.gradle.kts; compares both the JVM.apipair and the native.klib.apipair as sets of class-qualified signatures.api-parityjob in.github/workflows/api-check.yml(ubuntu — needs no macOS host). Also hooked into the rootchecklifecycle and runnable locally via./gradlew checkApiParity.docs/api-parity.md(contract, why parity matters, every exclusion, and the fix-it workflow) + an extensively-commented task + a README contributing pointer.Verification (light checks only — no
build/assemble/iOS-native run)Green (parity holds):
Red — drop
clear ()Vfrom the noop JVM dump → FAILS:Red — drop
presentSharinganfrom the noop native dump → FAILS:Both divergences were reverted; the check returns to green.
./gradlew apiCheck—BUILD SUCCESSFUL, committed dumps are current.Implementation note
The comparison helpers are local functions inside the task action rather than script-level functions: Gradle Kotlin DSL silently drops top-level statements placed after top-level
fundeclarations (and statements can't forward-reference functions declared later), so a top-level helper layout left the task unregistered. Keeping the logic insidedoLastsidesteps both.Unrelated pre-existing
site/assets/*.cssworking-tree edits were intentionally left out of this PR.🤖 Generated with Claude Code